<?php

/**
 * Document model class.
 */
class DocModel extends CModel
{
	/**
	 * @var XS
	 */
	private $_xs;

	/**
	 * @var XSDocument
	 */
	private $_doc;

	public static function subValue($model, $attribute, $maxlen = null)
	{
		$value = CHtml::value($model, $attribute);
		if ($maxlen !== null && is_string($value) && strlen($value) > $maxlen) {
			$end = $maxlen;
			while ((ord($value[$end]) & 0xc0) == 0x80) {
				$end--;
			}
			$value = substr($value, 0, $end) . '...';
		}
		return $value;
	}

	/**
	 * Constructor.
	 * @param XS $xs object of xunsearch project.
	 * @param XSDocument $doc object of related xunsearch document
	 * @see getScenario
	 */
	public function __construct($xs, $doc = null)
	{
		$this->_xs = is_string($xs) ? Yii::app()->pf->get($xs) : $xs;
		$this->_doc = $doc === null ? new XSDocument() : $doc;
		$this->init();
		$this->attachBehaviors($this->behaviors());
		$this->afterConstruct();
	}

	/**
	 * Initializes this model.
	 * This method is invoked in the constructor right after {@link scenario} is set.
	 * You may override this method to provide code that is needed to initialize the model (e.g. setting
	 * initial property values.)
	 */
	public function init()
	{
		
	}

	/**
	 * Returns the list of attribute names.
	 * @return array list of attribute names.
	 */
	public function attributeNames()
	{
		return array_keys($this->_xs->getAllFields());
	}

	public function attributeLabels()
	{
		$labels = array();
		foreach ($this->attributeNames() as $name) {
			$labels[$name] = implode('_', array_map('ucfirst', explode('_', $name)));
		}
		return $labels;
	}

	/**
	 * Returns the list of sortable attribute names
	 */
	public function sortAttributes()
	{
		$names = array();
		foreach ($this->_xs->getAllFields() as $field) {
			if ($field->isNumeric()) {
				$names[] = $field->name;
			}
		}
		return $names;
	}

	/** 	
	 * @return XS
	 */
	public function getXS()
	{
		return $this->_xs;
	}

	/** 	
	 * @return XSDocument
	 */
	public function getXSDocument()
	{
		return $this->_doc;
	}

	/**
	 * PHP sleep magic method.
	 * @return array
	 */
	public function __sleep()
	{
		return $this->attributeNames();
	}

	/**
	 * PHP getter magic method.
	 * This method is overridden so that document attributes can be accessed like properties.
	 * @param string $name property name
	 * @return mixed property value
	 */
	public function __get($name)
	{
		$fields = $this->_xs->getAllFields();
		if (isset($fields[$name])) {
			return $this->_doc->f($name);
		}
		return parent::__get($name);
	}

	/**
	 * PHP setter magic method.
	 * This method is overridden so that AR attributes can be accessed like properties.
	 * @param string $name property name
	 * @param mixed $value property value
	 */
	public function __set($name, $value)
	{
		$fields = $this->_xs->getAllFields();
		if (isset($fields[$name])) {
			return $this->_doc->setField($name, $value);
		}
		return parent::__set($name, $value);
	}

	/**
	 * Checks if a property value is null.
	 * This method overrides the parent implementation by checking
	 * if the named attribute is null or not.
	 * @param string $name the property name or the event name
	 * @return boolean whether the property value is null
	 */
	public function __isset($name)
	{
		$fields = $this->_xs->getAllFields();
		return isset($fields[$name]) ? true : parent::__isset($name);
	}

	/**
	 * Sets a component property to be null.
	 * This method overrides the parent implementation by clearing
	 * the specified attribute value.
	 * @param string $name the property name or the event name
	 */
	public function __unset($name)
	{
		$fields = $this->_xs->getAllFields();
		if (isset($fields[$name])) {
			return $this->_doc->setField($name, null);
		}
		return parent::__unset($name);
	}

	/**
	 * Calls the named method which is not a class method.
	 * Do not call this method. This is a PHP magic method that we override
	 * to implement the named scope feature.
	 * @param string $name the method name
	 * @param array $parameters method parameters
	 * @return mixed the method return value
	 */
	public function __call($name, $parameters)
	{
		// XSDocument: docid, rank, percent, weight, ccount
		if (in_array($name, array('docid', 'rank', 'percent', 'weight', 'ccount', 'matched'))) {
			return $this->_doc->__call($name, $parameters);
		}
		return parent::__call($name, $parameters);
	}

	/**
	 * Returns all column attribute values.
	 * Note, related objects are not returned.
	 * @param mixed $names names of attributes whose value needs to be returned.
	 * If this is true (default), then all attribute values will be returned, including
	 * those that are not loaded from DB (null will be returned for those attributes).
	 * If this is null, all attributes except those that are not loaded from DB will be returned.
	 * @return array attribute values indexed by attribute names.
	 */
	public function getAttributes($names = true)
	{
		$attrs = array();
		foreach ($this->attributeNames() as $name) {
			if ($names === true || isset($names[$name])) {
				$attrs[$name] = $this->$name;
			}
		}
		return $attrs;
	}

	/**
	 * Dynamic validate rules
	 */
	public function rules()
	{
		$rules = array();
		$safes = $dates = $numerics = array();
		foreach ($this->_xs->getAllFields() as $name => $field) {
			switch ($field->type) {
				case XSFieldMeta::TYPE_ID:
					$rules[] = array($name, 'required');
					break;
				case XSFieldMeta::TYPE_DATE:
					$dates[] = $name;
					break;
				case XSFieldMeta::TYPE_NUMERIC:
					$numerics[] = $name;
					break;
				default:
					$safes[] = $name;
					break;
			}
		}
		if (count($dates) > 0) {
			$rules[] = array(implode(',', $dates), 'date', 'format' => 'yyyyMMdd');
		}
		if (count($numerics) > 0) {
			$rules[] = array(implode(',', $numerics), 'numerical');
		}
		if (count($safes) > 0) {
			$rules[] = array(implode(',', $safes), 'safe');
		}
		return $rules;
	}

	public function getIsRemoved()
	{
		return $this->hasRemovedCache();
	}

	public function getPrimaryKey()
	{
		return $this->__get(strval($this->_xs->getFieldId()));
	}

	public function save($runValidation = true)
	{
		if ((!$runValidation || $this->validate()) && $this->beforeSave()) {
			try {
				$this->_xs->index->update($this->_doc);
				$this->_xs->index->flushIndex();
			} catch (XSException $e) {
				return false;
			}
			$this->delRemovedCache();
			$this->afterSave();
			return true;
		}
		return false;
	}

	public function deleteByPk($pk)
	{
		try {
			$index = $this->_xs->index;
			$index->del($pk);
			$index->flushIndex();
			$this->addRemovedCache($pk);
			return true;
		} catch (XSException $e) {
			//throw new CHttpException(500, $e->getMessage());
			return false;
		}
	}

	public function delete()
	{
		if ($this->beforeDelete()) {
			$key = strval($this->_xs->getFieldId());
			if ($this->deleteByPk($this->$key)) {
				$this->afterDelete();
				return true;
			}
		}
		return false;
	}

	/**
	 * @param mixed $query
	 * @return DocModel[]
	 */
	public function findAll($query)
	{
		$models = array();
		$docs = $this->doSearch($query);
		if (count($docs) === 1) {
			$this->_doc = $docs[0];
			$models[] = $this;
		} elseif (count($docs) > 1) {
			$className = get_class($this);
			foreach ($docs as $doc) {
				$models[] = new $className($this->_xs, $doc);
			}
		}
		return $models;
	}

	/**
	 * @param mixed $query
	 * @return DocModel
	 */
	public function find($query)
	{
		if (!is_array($query)) {
			$query = array('query' => $query);
		}
		$query['limit'] = 1;
		$docs = $this->doSearch($query);
		if (count($docs) === 0) {
			return null;
		}
		$this->_doc = $docs[0];
		return $this;
	}

	/**
	 * @return DocModel
	 * @throw CHttpException
	 */
	public function findByPk($pk)
	{
		return $this->find(strval($this->_xs->getFieldId()) . ':' . $pk);
	}

	/**
	 * Clean index data
	 * @throw CHttpException
	 */
	public function clean()
	{
		$this->_xs->index->clean();
		$this->cleanRemovedCache();
	}

	/**
	 * @param CActiveForm $form
	 * @param string $name
	 */
	public function inputEx($form, $name, $htmlOptions = array())
	{
		$field = $this->_xs->getField($name);
		if ($field->type === XSFieldMeta::TYPE_BODY || ($field->type === XSFieldMeta::TYPE_STRING && $field->hasIndexMixed() && !$field->hasCustomTokenizer())) {
			if (!isset($htmlOptions['rows']) && $field->type === XSFieldMeta::TYPE_BODY) {
				$htmlOptions['rows'] = 4;
			}
			return $form->textArea($this, $name, $htmlOptions);
		}
		return $form->textField($this, $name, $htmlOptions);
	}

	/**
	 * Events defined similiar with CActiveRecord
	 */
	public function onBeforeSave($event)
	{
		$this->raiseEvent('onBeforeSave', $event);
	}

	public function onAfterSave($event)
	{
		$this->raiseEvent('onAfterSave', $event);
	}

	public function onBeforeDelete($event)
	{
		$this->raiseEvent('onBeforeDelete', $event);
	}

	public function onAfterDelete($event)
	{
		$this->raiseEvent('onAfterDelete', $event);
	}

	protected function beforeSave()
	{
		if ($this->hasEventHandler('onBeforeSave')) {
			$event = new CModelEvent($this);
			$this->onBeforeSave($event);
			return $event->isValid;
		}
		return true;
	}

	protected function afterSave()
	{
		if ($this->hasEventHandler('onAfterSave')) {
			$this->onAfterSave(new CEvent($this));
		}
	}

	protected function beforeDelete()
	{
		if ($this->hasEventHandler('onBeforeDelete')) {
			$event = new CModelEvent($this);
			$this->onBeforeDelete($event);
			return $event->isValid;
		}
		return true;
	}

	protected function afterDelete()
	{
		if ($this->hasEventHandler('onAfterDelete')) {
			$this->onAfterDelete(new CEvent($this));
		}
	}

	protected function addRemovedCache($value = null)
	{
		if (($cache = Yii::app()->cache) !== null) {
			if ($value === null) {
				$value = $this->getPrimaryKey();
			}
			$key = $this->getRemovedCacheKey();
			if (($data = $cache->get($key)) === false) {
				$data = array();
			}
			if (!is_array($value)) {
				$data[$value] = true;
			} else {
				foreach ($value as $_value) {
					$data[$_value] = true;
				}
			}
			$cache->set($key, $data, 1800);
		}
	}

	public function delRemovedCache($value = null)
	{
		if (($cache = Yii::app()->cache) !== null) {
			if ($value === null) {
				$value = $this->getPrimaryKey();
			}
			$key = $this->getRemovedCacheKey();
			if (($data = $cache->get($key)) !== false && isset($data[$value])) {
				unset($data[$value]);
				$cache->set($key, $data, 1800);
			}
		}
	}

	public function hasRemovedCache($value = null)
	{
		if (($cache = Yii::app()->cache) !== null) {
			if ($value === null) {
				$value = $this->getPrimaryKey();
			}
			$key = $this->getRemovedCacheKey();
			if (($data = $cache->get($key)) !== false && isset($data[$value])) {
				return true;
			}
		}
		return false;
	}

	public function cleanRemovedCache()
	{
		if (($cache = Yii::app()->cache) !== null) {
			$key = $this->getRemovedCacheKey();
			if (($data = $cache->get($key)) !== false) {
				$cache->delete($key);
			}
		}
	}

	/**
	 * @return cache key
	 */
	private function getRemovedCacheKey()
	{
		return $this->_xs->getName() . '.' . get_class($this) . '.removed';
	}

	/**
	 * @param mixed $query 搜索字符串或数组配置, 若为数组支持以下键名(除 'query' 外可选)
	 * <pre>
	 * array(
	 *   'query' => '...',	// 查询语句
	 *   'order' => '...',	// 排序字段, 若为数组则采用 '字段名' => true/false 表示正序/逆序, 默认相关度
	 *   'limit' => 10,		// 返回条数, 默认为 XSSearch::PAGE_SIZE 即 10 条
	 *   'offset' => 0,		// 返回结果跳过匹配条数, 默认为 0
	 *   'fuzzy' => false,	// 是否默认为模糊搜索 ...
	 *   '...',				// 其它设置建议直接采用 XSSearch 对象去操作
	 * )
	 * </pre>
	 * @return XSDocument[]
	 */
	private function doSearch($query)
	{
		if (!is_array($query)) {
			$query = array('query' => $query);
		}
		if (!isset($query['query'])) {
			$query['query'] = '';
		}
		try {
			$search = $this->_xs->search;
			$search->setFuzzy(isset($query['fuzzy']) && $query['fuzzy'] === true)->setQuery($query['query']);
			if (isset($query['limit'])) {
				$search->setLimit(intval($query['limit']), isset($query['offset']) ? intval($query['offset']) : 0);
			}
			$search->setSort(isset($query['order']) ? $query['order'] : null);
			return $search->search();
		} catch (XSException $e) {
			if ($e->getCode() === CMD_ERR_NODB) {
				return array();
			}
			throw new CHttpException(500, $e->getMessage());
		}
	}
}
